package protocol.arq;
import protocol.hdlc.HDLCProtocol;
import test.Test;
import utils.advancedSocket.AdvancedSocket;
import utils.advancedSocket.AdvancedSocketCloseEvent;
import utils.advancedSocket.AdvancedSocketEvent;
import utils.advancedSocket.AdvancedSocketListener;
import utils.trame.MalformedTrameException;
import utils.trame.Trame;

/**
 * Represente la methode <i>Automatic Repeat reQuest</i> de controle d'erreur
 * pour la tranmission de donnees.
 * 
 * @see ARQSender
 * @see ARQReceiver
 * @author Bertholet Damien & Daoust Julien
 */
public abstract class ARQ {

	/**
	 * La taille de la fenetre.
	 */
	protected final int windowSize = HDLCProtocol.calculWindowSize(getMethod());

	private MessageBuilder messageBuilder = new MessageBuilder(
	        HDLCProtocol.FANION);
	private AdvancedSocket advancedSocket;

	/**
	 * Constructeur.
	 * 
	 * @param advancedSocket
	 *            Le {@link AdvancedSocket} de communication.
	 */
	public ARQ(AdvancedSocket advancedSocket) {
		this.advancedSocket = advancedSocket;

		advancedSocket.addAdvancedSocketListener(getAdvancedSocketListener());
	}

	/**
	 * Envoie la {@link Trame}.
	 * 
	 * @param trame
	 *            La {@link Trame}.
	 */
	protected void sendTrame(Trame trame) {
		advancedSocket.send(ARQ.encodeTrame(trame));
	}

	/**
	 * Est appele quand on recoit un message de l'interlocuteur representant une
	 * {@link Trame}.
	 * 
	 * @param data
	 *            Les donnees du message representant la {@link Trame} dans un
	 *            string binaire.
	 */
	protected abstract void receivedTrame(String data);

	/**
	 * Renvoie l'id de la methode correspondante (0 pour Go-Back-N, ...)..
	 * 
	 * @return L'id de la methode correspondante (0 pour Go-Back-N, ...)..
	 */
	protected abstract int getMethod();

	/**************************************************************************
	 ******************** AdvancedSocketListener fonctions ********************
	 *************************************************************************/

	/**
	 * Construit un {@link AdvancedSocketListener} pour gerer les
	 * receptions/envoies.
	 * 
	 * @return L'{@link AdvancedSocketListener}.
	 */
	private AdvancedSocketListener getAdvancedSocketListener() {
		return new AdvancedSocketListener() {

			/**
			 * On stocke chaque caractere dans un {@link StringBuffer} (on
			 * aurait pu utiliser une {@link String} mais cela aurait ete d'une
			 * grande perte d'efficacite. Quand notre message commence et finit
			 * par un fanion, on envoie les donnees entre les flags qui
			 * representent la {@link Trame} a la fonction 'receiveTrame'.
			 */
			@Override
			public void onReceiving(AdvancedSocketEvent event) {
				boolean isComplete = messageBuilder.add(event.getChar());

				if(isComplete) {
					String message = messageBuilder.putFirstMessage();

					message = Test.alterMessage(message);
					if(message == null)
						return;

					receivedTrame(message);
				}
			}

			/*
			 * (non-Javadoc)
			 * @see utils.advancedSocket.AdvancedSocketListener#onSending
			 * (utils. advancedSocket .AdvancedSocketEvent)
			 */
			@Override
			public void onSending(AdvancedSocketEvent event) {
				if(HDLCProtocol.VERBOSE) {
					System.out.println("Send :" + event.getData());
					System.out.println(ARQ.decodeTrame(event.getData())
					        .toExpliciteString() + "\n");
				}
			}

			/*
			 * (non-Javadoc)
			 * @see
			 * utils.advancedSocket.AdvancedSocketListener#onCloseConnection
			 * (utils. advancedSocket.AdvancedSocketCloseEvent)
			 */
			@Override
			public void onCloseConnection(AdvancedSocketCloseEvent event) {
				if(!HDLCProtocol.VERBOSE)
					return;

				if(event.isConnectionError())
					System.out.println("CloseConnectionError :" +
					        event.getErrorMessage());
				else
					System.out.println("CloseConnection");
			}
		};
	}

	/**
	 * Ferme la connexion a cause d'une erreur.
	 * 
	 * @param message
	 *            Le message de l'erreur.
	 */
	protected void closeConnectionError(String message) {
		advancedSocket.closeConnectionError(message);
	}

	/**
	 * Ferme la connexion.
	 */
	protected void closeConnection() {
		advancedSocket.close();
	}

	/**************************************************************************
	 **************************** Static functions ****************************
	 *************************************************************************/
	/**
	 * Transforme la {@link Trame} en string binaire et ajout les bits stuffing
	 * a cette string binaire ainsi que les flags/fanions.
	 * 
	 * @param trame
	 *            La {@link Trame}.
	 * @return La string binaire.
	 */
	public static String encodeTrame(Trame trame) {
		String binaryString = trame.toString();

		binaryString = binaryString.replaceAll("11111", "111110");

		return HDLCProtocol.FANION + binaryString + HDLCProtocol.FANION;
	}

	/**
	 * Change la chaine binaire recu en {@link Trame}. Les flags/fanions sont
	 * enleves ainsi que les bits stuffing. Et le checksum est verifie. Renvoie
	 * une {@link MalformedTrameException} pour une erreur sur la {@link Trame}.
	 * 
	 * @param data
	 *            La string binaire representant la {@link Trame}.
	 * @return La {@link Trame} decode.
	 * @throws MalformedTrameException
	 *             Si la string binaire est mal forme pour la {@link Trame} ou
	 *             que le checksum est incorrecte.
	 */
	public static Trame decodeTrame(String data) throws MalformedTrameException {
		data = data.replaceAll(HDLCProtocol.FANION, "");

		data = data.replaceAll("111110", "11111");

		Trame trame = new Trame(data);
		if(!trame.isCorrect())
			throw new MalformedTrameException("Checksum incorrect");

		return trame;
	}

	/**
	 * <p>
	 * Retourne l'index dans l'intervalle de la fenetre.
	 * </p>
	 * <p>
	 * Exemple : si index = -1 et que la fenetre est de taille 7, alors cela
	 * reverra 7.
	 * </p>
	 * 
	 * @param index
	 *            L'index.
	 * @param windowSize
	 *            La taille de la fenetre souhaite.
	 * @return Retourne l'index dans l'intervalle de la fenetre.
	 */
	protected int getIndexWindow(int index) {
		return (index + windowSize) % windowSize;
	}
}